<?php

/**
 * @copyright Michiel Hakvoort 2010
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD
 * @package mangrove
 * @subpackage grove
 * @filesource
 */

/*
 * Copyright (c) 2010 Michiel Hakvoort
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

namespace mg;

class BadInjectionException extends Exception {

}

class InjectionException extends Exception {

}

/**
 * An Injection represents a single stored value within a class implementing the Injectable interface.
 * When a class implements the Injectable interface all public static variables will be converted
 * to Injections when not already an Injection. If a public static variable is already an Injection it
 * will be used to read and write values to. Between initialization of a class implementing the Injectable
 * interface and destruction of the Injector class, Injections are "injected" so the public static variables
 * can be treated as normal public static variables.
 */
abstract class Injection {

    /**
     * The binding to the property of the class
     *
     * @var mixed
     */
    protected $propertyBinding;

    /**
     * The original content in the injection
     *
     * @var mixed
     */
    protected $initialValue;

    protected $previousValue;

    protected $isRevertState = false;

    protected $className = null;
    protected $id = null;
    protected $propertyName = null;

    /**
     * Construct a new Injection
     *
     * @param mixed $content
     * @param boolean $isRevertState
     */
    public function __construct($initialValue, $isRevertState = true) {
        $this->isRevertState = $isRevertState;

        if($isRevertState) {
            if(is_scalar($initialValue) || is_null($initialValue) || is_array($initialValue)) {
                $this->initialValue = $initialValue;
            } else {
                $this->initialValue = clone $initialValue;
            }
        }
    }

    /**
     * Initialize the Injection, informing it with which class and property it represents
     *
     * @access public
     *
     * @param string $className
     * @param string $propertyName
     */
    public function bind($instance, $className, $id, $propertyName) {
        $this->propertyBinding =& $instance->$propertyName;

        $this->className = $className;
        $this->propertyName = $propertyName;

        try {
            $value = $this->read();

            $this->setValue($value);

        } catch(BadInjectionException $e) {

        }
    }

    /**
     * Let the bound property take the given parameter as value
     *
     * @param mixed $value
     */
    protected function setValue($value) {
        $this->previousValue = is_object($value) ? clone $value : $value;
        $this->propertyBinding = $value;
    }

    /**
     * Determine whether the value represented should be cleared.
     *
     * @return bool True is the represented value should be cleared
     */
    protected function shouldClear() {
        return $this->isRevertState &&
        (is_object($this->initialValue)
        ? $this->initialValue == $this->propertyBinding
        : $this->initialValue === $this->propertyBinding) &&
        (is_object($this->initialValue)
        ? $this->initialValue != $this->previousValue
        : $this->initialValue !== $this->previousValue);
    }

    /**
     * Determine whether the value represented should be written out
     *
     * @return bool True if the represented value should be written out
     */
    protected function shouldWrite() {
        return (is_object($this->previousValue)
        ? $this->previousValue != $this->propertyBinding
        : $this->previousValue !== $this->propertyBinding);
    }

    /**
     * Clear or write the represented value
     */
    public function unbind() {
        if($this->shouldClear()) {
            $this->clear();
        } elseif($this->shouldWrite()) {
            $this->write($this->propertyBinding);
        }
    }

    /**
     * Clear the Injection from its content
     */
    protected abstract function clear();

    /**
     * Read the content from the backing store
     *
     * @return mixed
     */
    protected abstract function read();

    /**
     * Write the content to the backing store
     *
     * @param mixed $value
     */
    protected abstract function write($value);

}

/**
 * ExpirableInjections behave exact like normal Injections with the addition of being expirable.
 * An ExpirableInjection can act as a cache, as well as a constraint on data. If an ExpirableInjection
 * expires, it will be set to its default value.
 */
abstract class ExpirableInjection extends Injection {

    protected $ttl;
    protected $keepAlive;

    protected $startTime;
    /**
     * Enter description here...
     *
     * @param mixed $content The content to store
     * @param int $maxAge The maximum age in seconds before the content is expired
     * @param boolean $keepAlive If set to true, a pocket read will result in the the reset of the maxAge to its old value
     */
    public function __construct($initialValue, $ttl = 0, $keepAlive = false) {
        parent :: __construct($initialValue);

        $this->ttl = $ttl;
        $this->keepAlive = $keepAlive;
    }


    /**
     * {@inheritdoc}
     */
    public function bind($instance, $className, $id, $propertyName) {
        $this->propertyBinding =& $instance->$propertyName;

        $this->className = $className;
        $this->id = $id;
        $this->propertyName = $propertyName;

        try {
            $value = $this->read();

            // No age set, always use read value
            if($this->ttl <= 0) {
                $this->setValue($value);
                return;
            }

            if(!isset($value['startTime'])) {
                $this->startTime = time();
                $this->setValue($this->initialValue);
                return;
            }

            $startTime = $value['startTime'];

            // Time has passed, reset the instance
            if((time() - $startTime) > $this->ttl) {
                $this->startTime = time();
                $this->setValue($this->initialValue);

                return;
            }

            if($this->keepAlive) {
                $this->startTime = time();
            } else {
                $this->startTime = $startTime;
            }

            $this->setValue($value['value']);

        } catch(BadInjectionException $e) {
            $this->setValue($this->initialValue);
            $this->startTime = time();
        }
    }


    /**
     * {@inheritDoc}
     */
    public function unbind() {
        if($this->shouldClear()) {
            $this->clear();
            return;
        }

        if($this->ttl === 0) {
            if($this->shouldWrite()) {
                $this->write($this->propertyBinding);
            }

            return;
        }

        $pair = array();

        $pair['startTime'] = $this->startTime;
        $pair['value'] = $this->propertyBinding;

        $this->write($pair);
    }
}

/**
 * An Injection for sessions
 *
 * @author Michiel Hakvoort
 *
 */
class SessionInjection extends ExpirableInjection {

    /**
     * @var \mg\Session
     */
    protected $session = null;

    /**
     * Construct a new SessionInjection
     *
     * @param mixed $initialValue
     * @param int $ttl Time to live in seconds, 0 is infinity (after ttl seconds have expired, the value will be reset to its default)
     * @param bool $keepAlive If set to true, any access to this property will result in the ttl to be reset to its starting value
     */
    public function __construct($initialValue, $ttl = 0, $keepAlive = true) {
        parent :: __construct($initialValue, $ttl, $keepAlive);
    }

    /**
     * {@inheritDoc}
     */
    public function bind($instance, $className, $id, $propertyName) {
        $this->session = in(function(Session $session) { return $session; });

        parent :: bind($instance, $className, $id, $propertyName);
    }

    /**
     * {@inheritDoc}
     */
    protected function read() {
        $key = "mg.{$this->className}.{$this->id}.{$this->propertyName}";

        if(isset($this->session[$key])) {
            return $this->session[$key];
        }

        return $this->initialValue;

    }

    /**
     * {@inheritDoc}
     */
    protected function clear() {

        $key = "mg.{$this->className}.{$this->id}.{$this->propertyName}";

        if(!isset($this->session[$key])) {
            return;
        }

        unset($this->session[$key]);

    }

    /**
     * {@inheritDoc}
     */
    protected function write($value) {
        $key = "mg.{$this->className}.{$this->id}.{$this->propertyName}";

        $this->session[$key] = $value;

    }
}

/**
 * A \mg\SessionInjection which changes the session id when the value changes
 *
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 * @version 1.0
 * @see \mg\SessionInjection
 *
 */
class SecureSessionInjection extends SessionInjection {

    /**
     * {@inheritDoc}
     */
    protected function clear() {
        parent :: clear();

        $this->session->regenerateId();
    }

    /**
     * {@inheritDoc}
     */
    protected function write($value) {
        parent :: write($value);

        $this->session->regenerateId();
    }
}

/**
 *
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 * @version 1.0
 *
 */
class CookieInjection extends ExpirableInjection {

    protected $key;

    /**
     * @var \mg\CookieManager
     */
    protected $cookieManager = null;

    /**
     * 
     * @var int|null
     */
    protected $expiresAt;
    
    /**
     * Construct a CookieInjection
     *
     * @param mixed $content
     * @param int $ttl
     * @param boolean $keepAlive
     * @param string $key If specified, the content is encrypted with this key
     */
    public function __construct($initialValue, $ttl = 0, $keepAlive = true, $key = null) {
        parent :: __construct($initialValue, $ttl, $keepAlive);

        $this->key = $key;

        if($ttl === null) {
            $ttl = 0;
        }

        if($ttl !== 0) {
            $ttl += time();
        }

        $this->expiresAt = $ttl;
    }

    /**
     * {@inheritDoc}
     */
    public function bind($instance, $className, $id, $propertyName) {
        $this->cookieManager = in(function(CookieManager $cookieManager) { return $cookieManager; });

        parent :: bind($instance, $className, $id, $propertyName);
    }

    /**
     * {@inheritDoc}
     */
    protected function read() {
        // Obfuscate cookie keys
        $key = md5($this->className . $this->id . $this->propertyName);

        $value = $this->cookieManager->getCookie($key);

        if($value === null) {
            return $this->initialValue;
        }

        try {
            if($this->key !== null && extension_loaded('mcrypt')) {
                $iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB), MCRYPT_RAND);
                $content = unserialize(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $this->key, base64_decode($value), MCRYPT_MODE_ECB, $iv));
            } else {
                $value = base64_decode($value);
                for($i = 0; $i < mb_strlen($value); $i++) {
                    $value{$i} = $value{$i} ^ $key{$i % mb_strlen($key)};
                }
                $value = unserialize($value);
            }
        } catch(\Exception $e) {
            throw new BadInjectionException();
        }

        return $value;
    }

    /**
     * {@inheritDoc}
     */
    protected function clear() {
        $key = md5($this->className . $this->id . $this->propertyName);

        $this->cookieManager->invalidateCookie($key);

    }

    /**
     * {@inheritDoc}
     */
    protected function write($value) {
        // Obfuscate keys

        // TODO generalize this
        $key = md5($this->className . $this->id . $this->propertyName);

        if($this->key !== null && extension_loaded('mcrypt')) {
            $iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB), MCRYPT_RAND);
            $value = base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $this->key, serialize($value), MCRYPT_MODE_ECB, $iv));
        } else {
            $value = serialize($value);

            for($i = 0; $i < mb_strlen($value); $i++) {
                $value{$i} = $value{$i} ^ $key{$i % mb_strlen($key)};
            }

            $value = base64_encode($value);
        }

        $this->cookieManager->setCookie($key, $value, $this->expiresAt);
    }
}

/**
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 * @version 1.0
 */
class PropertyInjection extends Injection {

    protected $resourceName = null;

    /**
     * @var \mg\Properties
     */
    protected $properties = null;

    public function __construct($initialValue = null, $resourceName = null) {
        parent :: __construct($initialValue);

        $this->resourceName = $resourceName;
    }

    /**
     * {@inheritDoc}
     */
    public function bind($instance, $className, $id, $propertyName) {
        $this->properties = in(function(Properties $properties) {
            return $properties;
        });

        if($this->resourceName === null) {

            $p = mb_strtolower($propertyName);
            $this->resourceName = str_replace('\\', '.', $className) . ".{$p}";

        }

        parent :: bind($instance, $className, $id, $propertyName);


    }

    /**
     * {@inheritDoc}
     */
    protected function read() {
        if(isset($this->properties[$this->resourceName])) {
            return $this->properties[$this->resourceName];
        }

        return $this->initialValue;
    }

    /**
     * {@inheritDoc}
     */
    protected function clear() {

    }

    /**
     * {@inheritDoc}
     */
    protected function write($value) {

    }
}

